﻿#region Using
using System;
using System.IO;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using JigLibX.Physics;
using JigLibX.Geometry;
using JigLibX.Collision;
using JigLibX.Math;
#endregion Using

namespace LloydGame
{
    public abstract class ParticleSystem : GameComponent
    {
        #region Class Fields
        protected Effect m_effect;
        protected Texture2D m_particleTexture;

        protected Particle[] m_particles;
        protected Stack<Particle> m_deadParticles;
        protected int m_nrAliveParticles;
        protected BoundingBox m_bound;

        protected VertexPositionColorTexture[] m_shapes;
        protected int[] m_shapeVertIndexData;
        protected VertexDeclaration m_vertexDecl = null;

        protected Vector3 m_spawnPosition;
        protected bool m_respawnParticles = false;
        #endregion

        #region Properties
        public bool respawnParticles
        {
            set
            {
                m_respawnParticles = value;
            }
        }
        #endregion

        #region Constructor
        public ParticleSystem(Game p_game, int p_nrParticles, Vector3 p_position, bool p_respawnParticles)
            : base(p_game)
        {
            m_spawnPosition = p_position;
            m_respawnParticles = p_respawnParticles;

            // initiate the particles
            m_particles = new Particle[p_nrParticles];

            m_deadParticles = new Stack<Particle>();

            for (int i = 0; i < p_nrParticles; i++)
            {
                m_particles[i] = new Particle();

                if (p_respawnParticles)
                {
                    setParticleDefaults(m_particles[i]);
                }
                else
                {
                    m_deadParticles.Push(m_particles[i]);
                }
            }

            // each particle needs 4 vertices and 6 indices to draw the system as a triangle list
            m_shapes = new VertexPositionColorTexture[p_nrParticles * 4];
            m_shapeVertIndexData = new int[p_nrParticles * 6];

            for (int i = 0; i < p_nrParticles; i++)
            {
                m_shapes[i * 4] = new VertexPositionColorTexture(new Vector3(), Color.White, new Vector2(0, 0));
                m_shapes[i * 4 + 1] = new VertexPositionColorTexture(new Vector3(), Color.White, new Vector2(1, 0));
                m_shapes[i * 4 + 2] = new VertexPositionColorTexture(new Vector3(), Color.White, new Vector2(1, 1));
                m_shapes[i * 4 + 3] = new VertexPositionColorTexture(new Vector3(), Color.White, new Vector2(0, 1));
            }

            m_vertexDecl = new VertexDeclaration(
                p_game.GraphicsDevice,
                VertexPositionColorTexture.VertexElements);

            m_effect = Game.Content.Load<Effect>("Effects\\ParticleEffect");
        }
        #endregion

        #region Update
        public bool update(GameTime p_gameTime)
        {
            m_nrAliveParticles = 0;

            m_bound = new BoundingBox();

            for (int i = 0; i < m_particles.Length; i++)
            {
                if (!m_particles[i].active)
                    continue;

                updateParticle(i, p_gameTime);

                // if the particle is dead, either respawn it or move on to the next particle
                if (m_particles[i].energy <= 0)
                {
                    if (m_respawnParticles)
                    {
                        setParticleDefaults(m_particles[i]);
                    }
                    else
                    {
                        m_deadParticles.Push(m_particles[i]);

                        continue;
                    }
                }

                // setup the particle for rendering
                setupShape(i);

                // setup the indices for the first triangle of the particle
                m_shapeVertIndexData[m_nrAliveParticles * 6]     = i * 4 + 2;
                m_shapeVertIndexData[m_nrAliveParticles * 6 + 1] = i * 4 + 3;
                m_shapeVertIndexData[m_nrAliveParticles * 6 + 2] = i * 4;

                // setup the indices for the second triangle of the particle
                m_shapeVertIndexData[m_nrAliveParticles * 6 + 3] = i * 4;
                m_shapeVertIndexData[m_nrAliveParticles * 6 + 4] = i * 4 + 1;
                m_shapeVertIndexData[m_nrAliveParticles * 6 + 5] = i * 4 + 2;

                // update the bounding box
                BoundingBoxHelper.AddPoint(m_particles[i].position, ref m_bound);

                m_nrAliveParticles++;
            }

            // return false if all particles are dead
            return m_nrAliveParticles != 0;
        }

        public List<Particle> spawnParticles(int p_nrParticles, Vector3 p_spawnPosition, Vector3 p_particleDir, GameObject p_gameObject)
        {
            List<Particle> spawnedParticles = new List<Particle>();

            m_spawnPosition = p_spawnPosition;

            while (p_nrParticles-- > 0 && m_deadParticles.Count > 0)
            {
                Particle awakenedParticle = m_deadParticles.Pop();

                awakenedParticle.owner = p_gameObject;

                setParticleDefaults(awakenedParticle, p_particleDir);

                spawnedParticles.Add(awakenedParticle);
            }

            return spawnedParticles;
        }

        protected abstract void updateParticle(int p_nr, GameTime p_gameTime);

        protected abstract void setupShape(int p_nr);

        protected abstract void setParticleDefaults(Particle p_particle, Vector3 p_particleDir);

        protected virtual void setParticleDefaults(Particle p_particle)
        {
            setParticleDefaults(p_particle, p_particle.spawnDirection);
        }

        #endregion

        #region Draw
        public void draw(GameTime p_gameTime, Texture2D p_depthTex)
        {
            if (m_nrAliveParticles <= 0)
            {
                return;
            }

            // view frustum cull
            BoundingFrustum viewFrustum = new BoundingFrustum(((Game1)Game).camera.viewProj);

            if (viewFrustum.Contains(m_bound) == ContainmentType.Disjoint)
            {
                return;
            }

            // setup the particle effect
            m_effect.CurrentTechnique = m_effect.Techniques["Particle"];

            m_effect.Parameters["proj"].SetValue(((Game1)Game).camera.proj);

            m_effect.Parameters["particleTex"].SetValue(m_particleTexture);

            m_effect.Parameters["depthTex"].SetValue(p_depthTex);

            m_effect.Parameters["farClip"].SetValue(((Game1)Game).camera.farClip);

            int pass = p_depthTex == null ? 0 : 1;

            // backup the backface culling mode, none is used for particles
            CullMode cullModeBackup = Game.GraphicsDevice.RenderState.CullMode;
            Game.GraphicsDevice.RenderState.CullMode = CullMode.None;

            // draw the particles
            Game.GraphicsDevice.VertexDeclaration = m_vertexDecl;

            m_effect.Begin();
            m_effect.CurrentTechnique.Passes[pass].Begin();

            Game.GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionColorTexture>(
                Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleList,
                m_shapes,
                0,
                m_shapes.Length,
                m_shapeVertIndexData,
                0,
                m_nrAliveParticles * 2);

            m_effect.CurrentTechnique.Passes[pass].End();
            m_effect.End();

            // restore the backface culling mode
            Game.GraphicsDevice.RenderState.CullMode = cullModeBackup;
        }
        #endregion
    }
}
